using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Xml;
using System.Xml.Linq;
using System.Linq;


namespace GuavaData
{
    public class VMerge : Transform
    {
        string left;
        string right;
        List<string> keep;  //list of pk columns 
        List<string> rNonPk; //list right table non pk columns

        // Fields for run-time state of the columns
        List<string> inputKeysLeft;
        List<string> inputKeysRight;
        List<string> fkColumnsSeen;
        Dictionary<string, Domain.Domain> inputColumnsLeft;
        Dictionary<string, Domain.Domain> inputColumnsRight;
        List<string> mergeFKColumns;
        List<Condition> mergeFKConditions;
        List<string> rightFKChildren;
        List<string> leftFKChildren;

        //needed in order for VSplit.Inverse to work correctly...
        public bool revOutput;


        public VMerge(string leftTable, string rightTable)
        {
            if (leftTable == rightTable) throw new TransformException("The tables " + leftTable + ", " + rightTable + " to be VMerged can not be same");
            this.left = leftTable;
            this.right = rightTable;
            this.keep = new List<string>();
            this.rNonPk = new List<string>();
            this.fkColumnsSeen = new List<string>();
            mergeFKConditions = new List<Condition>();
            mergeFKColumns = new List<string>();
            rightFKChildren = new List<string>();
            leftFKChildren = new List<string>();

        }
    
        internal override void Expand(AddColumn ac, Command c)
        {
            //not either of the merged tables then we're done
            if (ac.Table != left && ac.Table != right)
                return;

            //column already exists? throw error
            if (this.inputColumnsLeft != null && this.inputColumnsRight != null){
            if (this.inputColumnsLeft.ContainsKey(ac.Column) || this.inputColumnsRight.ContainsKey(ac.Column))
                throw new Exception("Column " + ac.Column + " already exists in table " + ac.Table + ".");
            }
            // Add the column to the internal represenation
            if(ac.Table == left)
                this.inputColumnsLeft[ac.Column] = ac.Domain;
            else 
                this.inputColumnsRight[ac.Column] = ac.Domain;

            //add column to left table
            ac.Table = this.left;
        }

        internal override void Expand(AlterColumnRename acr, Command c)
        {
            //not either of the merged tables then we're done
            if (acr.Table != left && acr.Table != right)
                return;

            // If the column already exists, throw an error
            if (this.inputColumnsLeft.ContainsKey(acr.NewName) || this.inputColumnsRight.ContainsKey(acr.NewName))
                throw new Exception("Column " + acr.NewName + " already exists in table " + acr.Table + ", cant rename to this.");
            if ((!this.inputColumnsLeft.ContainsKey(acr.OldName))||(!this.inputColumnsRight.ContainsKey(acr.OldName)))
                throw new Exception("Column " + acr.NewName + " does not exist in table " + acr.Table + ", cant rename.");

            acr.Table = this.left;
            
            // Change the gui representaion of the columns if it's a regular column
            if (this.inputColumnsLeft.ContainsKey(acr.OldName))
            {
                this.inputColumnsLeft.Add(acr.NewName, this.inputColumnsLeft[acr.OldName]);
                this.inputColumnsLeft.Remove(acr.OldName);
            }
            else if (this.inputColumnsRight.ContainsKey(acr.OldName))
            {
                this.inputColumnsRight.Add(acr.NewName, this.inputColumnsRight[acr.OldName]);
                this.inputColumnsRight.Remove(acr.OldName);
            }




        }

        internal override void Expand(DropColumn dc, Command c)
        {
            //not either of the merged tables then we're done
            if (dc.Table != left && dc.Table != right)
                return;
            // If the column does not exist, throw error
            if (!(this.inputColumnsLeft.ContainsKey(dc.Column)) && !(this.inputColumnsRight.ContainsKey(dc.Column)))
                throw new Exception("Column " + dc.Column + " does not exist in table " + dc.Table + ".");

            dc.Table = this.left;
            if (dc.Table == left)
            {
                inputColumnsLeft.Remove(dc.Column);
            }
            else
            {
                inputColumnsRight.Remove(dc.Column);
            }

        }

        internal override void Expand(AddElement ae, Command c)
        {
            //not either of the merged tables then we're done
            if (ae.Table != left && ae.Table != right)
                return;
            if(!(inputColumnsLeft.ContainsKey(ae.Column)) && !(inputColumnsRight.ContainsKey(ae.Column)))
                throw new Exception("Column " + ae.Column + " does not exist in table "+ ae.Table + ", unable to add element " + ae.Element+ ".");

            if (ae.Table == right)
            {
                Domain.Enumerated eRight = this.inputColumnsRight[ae.Column] as Domain.Enumerated;
                if (eRight.Items.Contains(ae.Element))
                    throw new Exception("Domain already contains the element " + ae.Element + ".");
                ae.Table = left;
                eRight.Items.Add(ae.Element);
                inputColumnsRight[ae.Column] = eRight;
            }
            else
            {
                Domain.Enumerated eLeft = this.inputColumnsLeft[ae.Column] as Domain.Enumerated;
                if (eLeft.Items.Contains(ae.Element))
                    throw new Exception("Domain already contains the element " + ae.Element + ".");
                eLeft.Items.Add(ae.Element);
                inputColumnsLeft[ae.Column] = eLeft;
            }
        }

        internal override void Expand(AlterElementRename aer, Command c)
        {
            //not either of the merged tables then we're done
            if (aer.Table != left && aer.Table != right)
                return;

            if (!(inputColumnsLeft.ContainsKey(aer.Column)) && !(inputColumnsRight.ContainsKey(aer.Column)))
                throw new Exception("Column " + aer.Column + " does not exist in table " + aer.Table + ", unable to rename element " + aer.OldName + ".");

            if(aer.Table == left){
                Domain.Enumerated eLeft = this.inputColumnsLeft[aer.Column] as Domain.Enumerated;
                if(!eLeft.Items.Contains(aer.OldName))
                    throw new Exception("Element " + aer.OldName + " does not exist in the domain, cannot rename.");
                if(eLeft.Items.Contains(aer.NewName))
                    throw new Exception("Cannot rename element " + aer.OldName + ".  Domain already contains the element " + aer.NewName +".");
                eLeft.Items.Remove(aer.OldName);
                eLeft.Items.Add(aer.NewName);
                inputColumnsLeft[aer.Column]=eLeft;
            }else{
                if(aer.Table == right){
                    Domain.Enumerated eRight = this.inputColumnsRight[aer.Column] as Domain.Enumerated;
                    if(!eRight.Items.Contains(aer.OldName))
                        throw new Exception("Element " + aer.OldName + " does not exist in the domain, cannot rename.");
                    if(eRight.Items.Contains(aer.NewName))
                        throw new Exception("Cannot rename element " + aer.OldName + ".  Domain already contains the element " + aer.NewName +".");
                    eRight.Items.Remove(aer.OldName);
                    eRight.Items.Add(aer.NewName);
                    inputColumnsRight[aer.Column]=eRight;
                    aer.Table=left;
                }
            }
        }

        internal override void Expand(DropElement de, Command c)
        {
            //not either of the merged tables then we're done
            if (de.Table != left && de.Table != right)
                return;

            Domain.Enumerated eLeft = this.inputColumnsLeft[de.Column] as Domain.Enumerated;
            Domain.Enumerated eRight = this.inputColumnsRight[de.Column] as Domain.Enumerated;

            if (!(inputColumnsLeft.ContainsKey(de.Column)) && !(inputColumnsRight.ContainsKey(de.Column)))
                throw new Exception("Column " + de.Column + " does not exist in table " + de.Table + ", unable to drop element " + de.Element + ".");

            int i = eLeft.Items.IndexOf(de.Element) + eRight.Items.IndexOf(de.Element);
            if (i == 0)
                throw new Exception("Domain does not contain the element " + de.Element + ".");

            if (de.Table == left)
            {
                eLeft.Items.Remove(de.Element);
                inputColumnsLeft[de.Column] = eLeft;
            }
            else
            {
                eRight.Items.Remove(de.Element);
                inputColumnsRight[de.Column] = eRight;
                de.Table = left;
            }
        }

        internal override void Expand(AddTable at, Command c)
        {
            //not either of the merged tables then we're done
            if (at.Table != this.left && at.Table != right)
                return;

            //check for foreign keys coming through in the same transaction
            foreach (var com in c.container.Contents)
            {
                if (com.root is ForeignKey)
                {
                    ForeignKey fk = (ForeignKey)com.root;
                    //System.Console.WriteLine("Found fk from " + fk.ChildTable + " to " + fk.ParentTable);
                    if (fk.ChildTable.Equals(right) && fk.ParentTable.Equals(left))
                    {
                        if (fk.ChildConditions.Count > 0)
                            throw new Exception("Foreign key for a vmerge is not allowed to have child conditions.");
                        fkColumnsSeen.AddRange(fk.ParentColumns);
                        for (int i = 0; i < fk.ParentConditionColumns.Count; i++)
                        {
                            mergeFKColumns.Add(fk.ParentConditionColumns[i]);
                            mergeFKConditions.Add(fk.ParentConditions[i]);
                        }
                    }
                }
            }
            if (fkColumnsSeen.Count == 0)
                throw new Exception("No foreign keys found linking tables: " + left + " and " + right);
            
            // If the table left already exists, throw an error - table exists if the dictionary of columns is present
            if ((at.Table == left && this.inputColumnsLeft != null)||(at.Table == left && this.inputColumnsLeft != null))
            {
                throw new Exception("Table " + at.Table + " already exists.");
            }
            if (at.Table == this.right)
            {
                if (this.inputColumnsLeft == null)
                {
                    throw new Exception("Table " + left + " has to exist before table " + right + "can be merged.");
                }
                else
                {
                    inputKeysRight = new List<string>();
                    inputColumnsRight = new Dictionary<string, GuavaData.Domain.Domain>();

                    List<String> rightKeys = at.Keys;
                    List<String> rightColumns = new List<String>(at.Columns);
                    List<Domain.Domain> rightDomains = new List<Domain.Domain> (
                        from d in at.Domains
                        select d.Clone);
                    List<Command> newCommands = new List<Command>();
                    Dictionary<string, Domain.Domain> newInputColumnsRight =  new Dictionary<string,GuavaData.Domain.Domain>();

                    foreach (string s in rightKeys)
                    {
                        if (!inputKeysLeft.Contains(s))
                            throw new Exception("Key " + s + " is not in table to be merged with.");
 
                        Domain.Enumerated eLeft = this.inputColumnsLeft[s] as Domain.Enumerated;
                        Domain.Enumerated eRight =  rightDomains[rightKeys.IndexOf(s)] as Domain.Enumerated;
                        if (eRight != null)
                            foreach (string dom in eRight.Items)
                            {
                                if (!eLeft.Items.Contains(dom))
                                    throw new Exception("The domain of key column " + s + " is not a subset of the table to be merged with.");
                            }
                        newInputColumnsRight.Add(s, rightDomains[rightColumns.IndexOf(s)]);
                    }
                    for (int i = 0; i < rightColumns.Count; i++ )
                    {
                        if (!inputKeysLeft.Contains(rightColumns[i]))
                        {
                            if (inputColumnsLeft.ContainsKey(rightColumns[i]))
                                throw new Exception("Column " + rightColumns[i] + " already exists in the merge table.");
                            newCommands.Add(new Command(new AddColumn(left, rightColumns[i], rightDomains[i].Clone)));
                            newInputColumnsRight.Add(rightColumns[i], rightDomains[i].Clone);
                        }
                    }
                    c.NotifyChange(newCommands.ToArray());
                    inputColumnsRight = newInputColumnsRight;
                    inputKeysRight = rightKeys;
                }
            }
            else
            {
                inputColumnsLeft = new Dictionary<string, GuavaData.Domain.Domain>();
                inputKeysLeft = new List<string>();
                inputKeysLeft = at.Keys;
                if (at.Columns != null)
                {
                    for (int i = 0; i < at.Columns.Count; i++)
                    {
                        this.inputColumnsLeft[at.Columns[i]] = at.Domains[i].Clone;
                    }
                }
            }

        }

        internal override void Expand(AlterTableRename atr, Command c)
        {
            //not either of the merged tables then we're done
            if (atr.OldName != left && atr.OldName != right)
                return;

            if (atr.OldName == right)
            {
                if(atr.NewName==left)
                    throw new DDLSideEffectException("Cannot rename a table to " + atr.NewName + " because a table already exists with that name.");
                this.right = atr.NewName;
                c.NotifyChange();
            }
            if (atr.OldName == left)
            {
                if (atr.NewName == right)
                    throw new DDLSideEffectException("Cannot rename a table to " + atr.NewName + " because a table already exists with that name.");
                this.left = atr.NewName;
            }
        }

        internal override void Expand(DropTable dt, Command c)
        {
            //not either of the merged tables then we're done
            if (dt.Table != left && dt.Table != right)
                return;
            if (dt.Table == left)
            {
                //for droping left, null all gui represetations (right iternals are nulled either way at bottom)
                inputColumnsLeft = null;
                inputKeysLeft = null;
            }
            else
            {
                //for the right drop all columns in from the right table that are not keys
                foreach (string s in this.inputColumnsRight.Keys)
                {
                    List<Command> dropColumnCommands = new List<Command>();
                    if (!inputKeysRight.Contains(s))
                    {
                        dropColumnCommands.Add(new Command(new DropColumn(left, s)));
                    }
                    c.NotifyChange(dropColumnCommands.ToArray());
                }
            }
            // for either left or right drop null right internals
            inputColumnsRight = null;
            inputKeysRight = null;
        }

        internal override void Expand(Delete d, Command c)
        {
            //not either of the merged tables then we're done
            if (d.Table != right)
                return;

            RowConstant rc = new RowConstant();
            List<string> cols = new List<string>();
            foreach (string s in inputColumnsRight.Keys)
            {
                if (inputKeysLeft.Contains(s))
                    continue;
                rc.AddValue(s, inputColumnsRight[s], null);
                cols.Add(s);
            }

            Update up = new Update(left, d.Columns, d.Conditions, cols, rc);
            c.NotifyChange(new Command(up));

        }

        internal override void Expand(ForeignKey fk, Command c)
        {
            if (!(fk.ChildTable == right) && !(fk.ParentTable == right))
                return;

            if (fk.ChildTable == right && fk.ParentTable == left)
            {
                c.NotifyChange();
                return;
            }

            if (fk.ParentTable == right)
            {
                fk.ParentTable = left;
                foreach (string s in fk.ParentColumns)
                {
                    if (inputKeysLeft.Contains(s))
                    {
                        c.NotifyChange();
                        break;
                    }
                }

            }
            else
            {
                fk.ChildTable = left;
                foreach (string s in fk.ChildConditionColumns)
                {
                    if (!inputKeysLeft.Contains(s))
                    {
                        c.NotifyChange();
                        break;
                    }
                }
            }

        }

        internal override void Expand(Insert i, Command c)
        {
            //not either of the merged tables then we're done
            if (i.Table != right)
                return;
            // Process the child
            i.Child.Visit(this, c);

            //if it's a row constant just point it to the merged table
            if (i.Child is RowConstant)
            {
                List<string> upColumns = new List<string>();
                List<string> upConditionColumns = new List<string>();
                List<object> conditionValues = new List<object>();
                //filter out key columns into equality conditions  
                RowConstant rc = new RowConstant();
                for (int j = 0; j < i.Columns.Count; j++)
                {
                    string col = i.Columns[j];
                    if (inputKeysLeft.Contains(col))
                    {
                        upConditionColumns.Add(col);
                        conditionValues.Add((i.Child as RowConstant).Fields[j]);
                    }
                    else
                    {
                        upColumns.Add(col);
                        rc.AddValue(i.Child.exposedColumns[j], i.Child.exposedDomains[j], (i.Child as RowConstant).Fields[j]);
                    }
                }

                Update up = new Update(left, upColumns, rc);
                for (int j = 0; j < upConditionColumns.Count; j++)
                    up.AddCondition(upConditionColumns[j], Condition.NewCondition(ConditionType.EQ, conditionValues[j]));
   
                c.NotifyChange(new Command(up));
            }
            // not a row constant, need to create an update for each tuple of the query
            else
            {
                //get the results of the query
                Transaction select = new Transaction(i.Child);
                DataSet ds = this.Process(select);
                if (!(ds.Tables.Count > 0))
                    throw new Exception("Query did not return table " + left + ".");
                DataTable dt = ds.Tables[0];

                // make a list for the updates
                List<Command> uplist = new List<Command>();

                //iterate over the rows in the data set to create updates
                foreach (DataRow dr in ds.Tables[0].Rows)
                {
                    RowConstant rc = new RowConstant();
                    List<string> condCols = new List<string>();
                    List<Condition> condVals = new List<Condition>();
                    List<string> cols = new List<string>();
                    foreach (DataColumn dc in dt.Columns)
                    {
                        if (!inputColumnsLeft.ContainsKey(dc.ColumnName) && !inputColumnsRight.ContainsKey(dc.ColumnName) && !inputKeysLeft.Contains(dc.ColumnName))
                            throw new Exception("Invalid column: " + dc.ColumnName + ", for table: " + left + ".");
                        if (inputKeysLeft.Contains(dc.ColumnName))
                        {

                            condCols.Add(dc.ColumnName);
                            condVals.Add(Condition.NewCondition(ConditionType.EQ, dr[dc]));
                        }
                        else
                        {
                            cols.Add(dc.ColumnName);
                            rc.AddValue(dc.ColumnName, inputColumnsLeft[dc.ColumnName], dr[dc]);
                        }
                    }
                    //Update up = new Update(left, cols, rc);

                    Update up = new Update(left, condCols, condVals, cols, rc);
                    uplist.Add(new Command(up));

                }
                c.NotifyChange(uplist.ToArray());
            }
        }

        internal override void Expand(TableRef tr, Command c)
        {
            //not either of the merged tables then we're done
            if (tr.Table != left && tr.Table != right)
                return;

            if (tr.Table == left)
            {
                TableRef trNew = new TableRef(left, tr.Alias);
                foreach (string s in inputColumnsLeft.Keys)
                {
                    if (tr.Columns.Contains(s))
                        trNew.AddColumn(s, tr.exposedColumns[tr.Columns.IndexOf(s)],
                            tr.exposedDomains[tr.Columns.IndexOf(s)]);
                }
 
                List<string> refCols = new List<string>();
                foreach (string s in trNew.Columns)
                {
                    if (inputColumnsLeft.ContainsKey(s))
                        refCols.Add(tr.exposedColumns[tr.Columns.IndexOf(s)]);
                }
                tr.parent.NotifyChange(tr, new Project(trNew, refCols));
                
            }
            else
            {
                string newAlias = c.nextTableAlias(this.left);
                TableRef tref = new TableRef(left, tr.Alias);
                foreach (string s in inputColumnsRight.Keys)
                {
                    if(tr.Columns.Contains(s))
                        tref.AddColumn(s, tr.exposedColumns[tr.Columns.IndexOf(s)],
                            tr.exposedDomains[tr.Columns.IndexOf(s)]);
                }
                Operator op = tref;
                if (mergeFKColumns.Count > 0)
                {
                    Filter fkFilter = new Filter(tref);
                    for (int i = 0; i < mergeFKColumns.Count; i++)
                        fkFilter.AddCondition(mergeFKColumns[i], mergeFKConditions[i]);
                    op = fkFilter;
                }
                Filter fil = new Filter(op);
                foreach(string s in inputColumnsRight.Keys){
                    if (!inputKeysRight.Contains(s))
                    {
                        List<object> cond = new List<object>();
                        fil.AddCondition(tref.Alias+ "." + s, Condition.NewCondition((ConditionType.ISNOTNULL)));
                    }
                }

                List<string> newExCols = new List<string>();
                foreach (string s in tref.Columns)
                {
                    if (inputColumnsRight.ContainsKey(s))
                        newExCols.Add(tr.exposedColumns[tr.Columns.IndexOf(s)]);
                }
                Project proj = new Project(fil, newExCols);
                if (tr.parent != null)
                    tr.parent.NotifyChange(tr, fil);
            }
        }

        internal override void Expand(Update u, Command c)
        {
            //not either of the merged tables then we're done
            if (u.Table != right&& u.Table != left)
                return;
            
            // Process the child
            u.Child.Visit(this, c);

            //change table to point to the merged
            u.Table = left;
           
        }
        public override Transform Inverse()
        {
            List<string> ls = new List<string>();
            foreach(var key in inputColumnsRight.Keys) ls.Add(key);
            VSplit vs = new VSplit(this.left, this.right, ls);
            return vs;
        }
        public override List<XElement> ToXElement()
        {
            XElement x = new XElement("Transform", new XAttribute("type", "VMerge"), 
                new XAttribute("left", left), new XAttribute("right", right));
            List<XElement> xList = new List<XElement>();
            if (!(this.Child is SqlProvider))
                xList = ((Transform)this.Child).ToXElement();
            xList.Add(x);
            return xList;
        }
        new public static VMerge FromXElement(XElement x)
        {
            string left = x.Attribute("left").Value;
            string right = x.Attribute("right").Value;
            if (!(left == null) && !(right == null))
                return new VMerge(left, right);
            else
                throw new Exception("Error while loading VMerge from XML");
        }
    }
}